
#include "3d_terrain.h"
#include "heightmap.h"
#include "graphics_functions.h"
#include "glsl.h"

using namespace WtfGraphics;
using namespace WtfEngine;

#define MAX_LOD 13
#define MIN_LOD 12
#define SEGMENTATION_START_LOD 12



Terrain::SegmentCreationData::SegmentCreationData(const WtfEngine::Heightmap::tRef& rHm,
		const Tileset::tCacheRef& rTs, const tVector& vSize, int iLevels)
: rHeightmap(rHm), rTileset(rTs), vScale(rHm->CalculateScale(vSize)), vSize(vSize), vvTriangle(iLevels),
vvNormal(iLevels), vvTiling(iLevels), vvVertexTiling(iLevels) {
	
	//
	// Construct triangle tree by subdivision.
	// Construct the first level artificially (containing two triangles to
	// make the rectangular terrain).
	//

	unsigned nTris = 2;
	for(int i = 0; i < iLevels; i++) {
		vvTriangle[i].resize(nTris);
		vvNormal[i].resize(nTris);
		vvTiling[i].resize(nTris);
		vvVertexTiling[i].resize(nTris * 3);
		nTris *= 2;
	};

	InitVertex(0, 0, 0, tVector(vScale[0], 0, vScale[2]));
	InitVertex(0, 0, 1, tVector(vSize[0]-(vScale[0]*2), 0, vScale[2]));
	InitVertex(0, 0, 2, tVector(vScale[0], 0, vSize[2]-(vScale[2]*2)));

	InitVertex(0, 1, 2, tVector(vSize[0]-(vScale[0]*2), 0, vScale[2]));
	InitVertex(0, 1, 0, tVector(vSize[0]-(vScale[0]*2), 0, vSize[2]-(vScale[2]*2)));
	InitVertex(0, 1, 1, tVector(vScale[0], 0, vSize[2]-(vScale[2]*2)));

	// Start subdivision
	CreateTriangle(1, 0, iLevels - 2);
	CreateTriangle(1, 1, iLevels - 2);
	CreateTriangle(1, 2, iLevels - 2);
	CreateTriangle(1, 3, iLevels - 2);
};

void Terrain::SegmentCreationData::InitVertex(int iLevel, int iPos, int iVert, const tPosition& vPos) {
	// Set height of vertex from heightmap
	tVector& vVert(vvTriangle[iLevel][iPos][iVert]);
	vVert = vPos;
	vVert[1] = rHeightmap->getHeight(vPos[0], vPos[2], vScale);

#	define CALC_VERTEX(i, j)	tVector(i, rHeightmap->getHeight(i, j, vScale), j)
#	define CALC_NORMAL(va, vb)	(va - vVert).cross(vb - vVert).normalize()

	// Calculate neighboring vertices
	tVector v1 = CALC_VERTEX(vPos[0], vPos[2]+vScale[2]);
	tVector v12 = CALC_VERTEX(vPos[0]+vScale[0], vPos[2]+vScale[2]);
	tVector v2 = CALC_VERTEX(vPos[0]+vScale[0], vPos[2]);
	tVector v23 = CALC_VERTEX(vPos[0]+vScale[0], vPos[2]-vScale[2]);
	tVector v3 = CALC_VERTEX(vPos[0], vPos[2]-vScale[2]);
	tVector v34 = CALC_VERTEX(vPos[0]-vScale[0], vPos[2]-vScale[2]);
	tVector v4 = CALC_VERTEX(vPos[0]-vScale[0], vPos[2]);
	tVector v41 = CALC_VERTEX(vPos[0]-vScale[0], vPos[2]+vScale[2]);

	// Calculate vertex normal, making sure it faces upwards
	vvNormal[iLevel][iPos][iVert] = (
		CALC_NORMAL(v1, v12) + CALC_NORMAL(v12, v2)	+ CALC_NORMAL(v2, v23) + CALC_NORMAL(v23, v3) +
		CALC_NORMAL(v3, v34) + CALC_NORMAL(v34, v4) + CALC_NORMAL(v4, v41) + CALC_NORMAL(v41, v1)
		
		//CALC_NORMAL(v1, v2) + CALC_NORMAL(v2,v3) + CALC_NORMAL(v3, v4) + CALC_NORMAL(v4, v1)
	).normalize();
	assert(vvNormal[iLevel][iPos][iVert].dot(tVector(0,1,0)) > 0);

	// Calculate vertex tiling
	rTileset->CalculateTiling(vvTriangle[iLevel][iPos][iVert], vvNormal[iLevel][iPos][iVert], vvVertexTiling[iLevel][(iPos * 3) + iVert]);
};

void Terrain::SegmentCreationData::CopyVertex(int iLevel, int iPos, int iVert, int iParentVert) {
	vvTriangle[iLevel][iPos][iVert] = vvTriangle[iLevel-1][iPos/2][iParentVert];
	vvNormal[iLevel][iPos][iVert] = vvNormal[iLevel-1][iPos/2][iParentVert];
	vvVertexTiling[iLevel][(iPos * 3) + iVert] = vvVertexTiling[iLevel-1][((iPos/2) * 3) + iParentVert];
};

void Terrain::SegmentCreationData::CreateTriangle(int iLevel, int iPos, int nSubs) {
	if(nSubs >= 0) {
		// Subdivide parent 1-2 edge to give 0 coordinate for this triangle, such that
		// the 1-2 edge will be the longest edge of this triangle. Triangles are constructed
		// counter clockwise.

		Triangle& triParent(vvTriangle[iLevel-1][iPos/2]);

		InitVertex(iLevel, iPos, 0, (triParent[1] + triParent[2]) * 0.5);
		InitVertex(iLevel, iPos, 2 - (iPos % 2), triParent[1 + (iPos % 2)]);
		InitVertex(iLevel, iPos, 1 + (iPos % 2), triParent[0]);
		
		// Calculate tiling for triangle
		tTiling& vTiling(vvTiling[iLevel][iPos]);
		
		for(int iVert = 0; iVert < 3; iVert++) {
			Tileset::tTiling& vVertexTiling(vvVertexTiling[iLevel][(iPos * 3) + iVert]);
			for(Tileset::tTiling::iterator i = vVertexTiling.begin(); i != vVertexTiling.end(); i++) {

				//
				// If the material is already assigned to the triangle, just set the vertex opacity. Otherwise,
				// add the material to the triangle.
				//

				tTiling::iterator j = std::find(vTiling.begin(), vTiling.end(), TileAssignment(i->rTileMaterial));
				if(j == vTiling.end()) {
					tVector vOpacity; vOpacity[iVert] = i->fOpacity;
					vTiling.push_back(TileAssignment(i->rTileMaterial, vOpacity));
				} else {
					j->vOpacity[iVert] = i->fOpacity;
				};
			};
		};
		
		// Subdivide
		CreateTriangle(iLevel + 1, iPos * 2, nSubs - 1);
		CreateTriangle(iLevel + 1, (iPos * 2) + 1, nSubs - 1);
	};
};


Terrain::Terrain(const WtfEngine::StringPool::Entry &sName)
: GraphicsData(sName) {
};

void Terrain::render() {
	Segment& seg(mvvSegment[0][0]);
	seg.rMaterial->Activate();
	seg.rMesh->render();
	seg.rMaterial->Deactivate();
};

void Terrain::GenerateMaps(int iLevel, int iSeg, SegmentCreationData * pData) {
	struct TriangleSet {
		Material::tRef rMaterial;
		std::vector<Triangle> vTriangle;
		std::vector<tVector> vvOpacity;

		inline TriangleSet(const Material::tRef& rMat): rMaterial(rMat) {};

		inline void AddTriangle(const Triangle& tri, const tVector& vOpacity) {
			vTriangle.push_back(tri);
			vvOpacity.push_back(vOpacity);
		};

		inline bool operator == (const TriangleSet& tri) const {
			return rMaterial == tri.rMaterial;
		};
	};

	typedef std::vector<TriangleSet> tTriangleSets;

	Segment& seg(mvvSegment[iLevel - MIN_LOD][iSeg]);
	FBOTexture::tRef rDiffuseMap = dynamic_cast<FBOTexture*>(
		&*dynamic_cast<DiffuseTextureMaterial*>(&*seg.rMaterial)->getDiffuseMap());
	tTriangleSets vTriangleSets;
	iVector2D vDim = rDiffuseMap->getSize();

	//
	// Create the triangle sets by grouping renders of the same material.
	//

	for(int iTri = 0; iTri < pData->vvTriangle[iLevel].size(); iTri++) {
		for(tTiling::iterator i = pData->vvTiling[iLevel][iTri].begin();
				i != pData->vvTiling[iLevel][iTri].end(); i++) {
			tTriangleSets::iterator its = std::find(vTriangleSets.begin(), vTriangleSets.end(),
				TriangleSet(i->rTileMaterial));

			if(its == vTriangleSets.end()) {
				vTriangleSets.push_back(TriangleSet(i->rTileMaterial));
				vTriangleSets.back().AddTriangle(pData->vvTriangle[iLevel][iTri], i->vOpacity);
			} else {
				its->AddTriangle(pData->vvTriangle[iLevel][iTri], i->vOpacity);
			};
		};
	};

	//
	// Render the triangle sets
	//

	glBindTexture(GL_TEXTURE_RECTANGLE_NV, 0);
	rDiffuseMap->Activate();
	glDrawBuffer(GL_COLOR_ATTACHMENT0_EXT);

	glClearColor(0,0,0,0);
	ClearScreen();
	SetupOrthoProjection(rDiffuseMap);
	EnableBlending();

	// Render background - default material
	Tileset::tTiling defaultTiling;
	pData->rTileset->CalculateTiling(tVector(), tVector(0,1,0), defaultTiling);
	defaultTiling[0].rTileMaterial->Activate();
	
	glBegin(GL_TRIANGLES);
	glColor4f(1.0,1.0,1.0,1.0);
	for(int iTri = 0; iTri < pData->vvTriangle[iLevel].size(); iTri++) {
		Triangle& tri(pData->vvTriangle[iLevel][iTri]);


#			define RENDER_TEXTURED_VERTEX_BG(i) \
			glVertex2f((float)vDim[0] * (tri[i][0] / pData->vSize[0]), (float)vDim[1] * (tri[i][2] / pData->vSize[2]))
		
		RENDER_TEXTURED_VERTEX_BG(0);
		RENDER_TEXTURED_VERTEX_BG(1);
		RENDER_TEXTURED_VERTEX_BG(2);
	};
	glEnd();
	defaultTiling[0].rTileMaterial->Deactivate();

	// XXX: OpenGL Specific
	for(tTriangleSets::iterator i = vTriangleSets.begin(); i != vTriangleSets.end(); i++) {
		std::cout << "Rendering " << (*i->rMaterial->getName()).c_str() << ": " << i->vTriangle.size() << " tris" << std::endl;

		i->rMaterial->Activate();
		glTranslatef(0,0,0.001);
		glBegin(GL_TRIANGLES);

		//
		// Draw the blended triangles, with the tile material activated. We are creating
		// the 2D diffuse map, so normalize vertex coordinates to [0,1] range, then multiply
		// by dimension of diffuse map to give correct location.
		//

		for(int iTri = 0; iTri < i->vTriangle.size(); iTri++) {
			Triangle& tri(i->vTriangle[iTri]);
			tVector& vOpacity(i->vvOpacity[iTri]);

#			define RENDER_TEXTURED_VERTEX(i) \
				glColor4f(1.0, 1.0, 1.0, vOpacity[i]); \
				glVertex2f((float)vDim[0] * (tri[i][0] / pData->vSize[0]), (float)vDim[1] * (tri[i][2] / pData->vSize[2]))

			RENDER_TEXTURED_VERTEX(0);
			RENDER_TEXTURED_VERTEX(1);
			RENDER_TEXTURED_VERTEX(2);
		};

		glEnd();
		i->rMaterial->Deactivate();
	};

	DisableBlending();
	rDiffuseMap->Deactivate();
};

void Terrain::Read(const WtfEngine::String &sName) {
	// Segment data
	tVector vSize = tVector(100, 40, 100);
	SegmentCreationData segData(Heightmap::tCacheRef(sName + "/Heightmap.bmp"),
		Tileset::tCacheRef(sName + "/Tileset.xml"), vSize, MAX_LOD);

	//
	// Start creating the meshes
	//

	unsigned nSegs = 1;
	mvvSegment.resize(MAX_LOD - MIN_LOD);
	for(int iLevel = MIN_LOD; iLevel < MAX_LOD; iLevel++) {
		mvvSegment[iLevel - MIN_LOD].resize(nSegs);
		Segment& seg(mvvSegment[iLevel - MIN_LOD][0]);


		// Generate/load diffuse map

		iVector2D vTexDim(2048,2048);
		FBOTexture::tRef rDiffuseMap = new FBOTexture();
		rDiffuseMap->Create(vTexDim);

		seg.rMaterial = new DiffuseTextureMaterial("terrain",
			DiffuseTextureMaterial::tCreationOptions(rDiffuseMap));
		GenerateMaps(iLevel, 0, &segData);

		// Write the texture
		rDiffuseMap->Write(sName + "/Diffuse.png");
		
		// Create texture coordinates
		std::vector<std::vector<fVector2D> > vvTexCoords;
		vvTexCoords.resize(1);

		std::vector<fVector2D>& vTexCoords(vvTexCoords[0]);

		for(int i = 0; i < segData.vvTriangle[iLevel].size(); i++) {
			for(int j = 0; j < 3; j++) {
				tVector& v(segData.vvTriangle[iLevel][i][j]);
				vTexCoords.push_back(fVector2D(v[0] / vSize[0], v[2] / vSize[2]));
			};
		};
		rDiffuseMap->ConvertNormalCoordinates(vTexCoords);

		// Create the mesh
		seg.rMesh = new Mesh("terrain");
		seg.rMesh->setTriangles(segData.vvTriangle[iLevel]);
		seg.rMesh->CreateVBO(&segData.vvNormal[iLevel],
			&vvTexCoords, false);
	};
};

WtfEngine::IDataObject * Terrain::Load(const WtfEngine::StringPool::Entry &sName) {
	Terrain * pTerrain = new Terrain(sName);
	pTerrain->Read(sName);
	return pTerrain;
};


/**
 * Diffuse material for tiling the terrain.
 **/
class TileMaterial: public Material {
	GLSLProgram::tRef mrShader;
	Texture::tRef mrTexture;

	int mTextureLocation, mTexSizeLocation, mTexScaleLocation;

public:
	TileMaterial(const WtfEngine::StringPool::Entry& sName): Material(sName) {};

	void Activate() {
		mrTexture->Bind();
		mrShader->Activate();
		glUniform1iARB(mTextureLocation, 0);
		glUniform2fARB(mTexSizeLocation, mrTexture->getWidth(), mrTexture->getHeight());
		glUniform2fARB(mTexScaleLocation, 10, 10);
	};

	void Deactivate() {
		mrShader->Deactivate();
		mrTexture->Release();
	};

	static WtfEngine::IDataObject * Load(const WtfEngine::StringPool::Entry& sName) {
		GLSLShader::tRef rVert, rFrag;
		TileMaterial * pMat = new TileMaterial(sName);
		
		pMat->mrShader = new GLSLProgram();
		rVert = new GLSLShader(); rVert->Load("src/shaders/3d_terrain_tileset_diffuse.vert");
		rFrag = new GLSLShader(); rFrag->Load("src/shaders/3d_terrain_tileset_diffuse.frag");
		pMat->mrShader->Load(rVert, rFrag);

		pMat->mTextureLocation = pMat->mrShader->GetUniformLocation("gDiffuse");
		pMat->mTexSizeLocation = pMat->mrShader->GetUniformLocation("gvTexSize");
		pMat->mTexScaleLocation = pMat->mrShader->GetUniformLocation("gvTexScale");
		checkGLError("fetching shader variable locations");

		pMat->mrTexture = WtfEngine::TextureImage::tCacheRef(*sName + ".jpg");
		return pMat;
	};

	DATA_LOADABLE(TileMaterial, GC_ADDEDSIZE_AUTO(Material, TileMaterial));
};


/**** Tileset ****/

void Tileset::CalculateTiling(const tVector &vPos, const tVector &vNorm, Tileset::tTiling &vTiles) {
	typedef TileMaterial::tCacheRef tTile;
	//if(vPos[1] > 25.0) {
	if(abs(vNorm.dot(tVector(0,1,0))) < 0.5) {
		vTiles.push_back(TileAssignment(tTile("media/textures/tiles/Dirt"), 1.0));
		std::cout << "DIRT\n";
	//} else if(vPos[1] > 20.0) {
	} else if(abs(vNorm.dot(tVector(0,1,0))) < 0.6) {
		vTiles.push_back(TileAssignment(tTile("media/textures/tiles/Dirt"), (vPos[1] - 20.0) / 5.0));
		vTiles.push_back(TileAssignment(tTile("media/textures/tiles/PatchyGrass"), (25.0 - vPos[1]) / 5.0));
	} else {
		if(vPos[1] > 25.0) {
			vTiles.push_back(TileAssignment(tTile("media/textures/tiles/Snow"), 1.0));
		} else if(vPos[1] > 20.0) {
			vTiles.push_back(TileAssignment(tTile("media/textures/tiles/PatchyGrass"), 1.0));
		} else {
			vTiles.push_back(TileAssignment(tTile("media/textures/tiles/Grass"), 1.0));
		};
	};
};

WtfEngine::IDataObject * Tileset::Load(const WtfEngine::StringPool::Entry &sName) {
	Tileset * pTileset = new Tileset(sName);
	return pTileset;
};


#include "pmodel.h"
void WtfGraphics::TestTerrain() {
	new Static(Static::tCreationOptions(), Terrain::tCacheRef("media/terrain/test"), new PhysicalModels::Oblong("", tVector(100,10,100)));
};
